home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Resources / Browsers, Managers & Extensions / Mozilla Weave 0.2.7 / latest-weave.xpi / modules / xmpp / xmppClient.js < prev   
Text File  |  2008-07-08  |  19KB  |  486 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Bookmarks Sync.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla.
  17.  * Portions created by the Initial Developer are Copyright (C) 2008
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Jono DiCarlo <jdicarlo@mozilla.org>
  22.  *
  23.  * Alternatively, the contents of this file may be used under the terms of
  24.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  25.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26.  * in which case the provisions of the GPL or the LGPL are applicable instead
  27.  * of those above. If you wish to allow use of your version of this file only
  28.  * under the terms of either the GPL or the LGPL, and not to allow others to
  29.  * use your version of this file under the terms of the MPL, indicate your
  30.  * decision by deleting the provisions above and replace them with the notice
  31.  * and other provisions required by the GPL or the LGPL. If you do not delete
  32.  * the provisions above, a recipient may use your version of this file under
  33.  * the terms of any one of the MPL, the GPL or the LGPL.
  34.  *
  35.  * ***** END LICENSE BLOCK ***** */
  36.  
  37. const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator'];
  38.  
  39. // See www.xulplanet.com/tutorials/mozsdk/sockets.php
  40. // http://www.xmpp.org/specs/rfc3920.html
  41. // http://www.process-one.net/docs/ejabberd/guide_en.html
  42. // http://www.xulplanet.com/tutorials/mozsdk/xmlparse.php
  43. // http://developer.mozilla.org/en/docs/xpcshell
  44. // http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests
  45.  
  46. // IM level protocol stuff: presence announcements, conversations, etc.
  47. // ftp://ftp.isi.edu/in-notes/rfc3921.txt
  48.  
  49. var Cc = Components.classes;
  50. var Ci = Components.interfaces;
  51. var Cu = Components.utils;
  52.  
  53. Cu.import("resource://weave/log4moz.js");
  54. Cu.import("resource://weave/xmpp/transportLayer.js");
  55. Cu.import("resource://weave/xmpp/authenticationLayer.js");
  56.  
  57. function XmppClient( clientName, realm, clientPassword, transport, authenticator ) {
  58.   this._init( clientName, realm, clientPassword, transport, authenticator );
  59. }
  60. XmppClient.prototype = {
  61.   //connection status codes:
  62.   NOT_CONNECTED: 0,
  63.   CALLED_SERVER: 1,
  64.   AUTHENTICATING: 2,
  65.   CONNECTED: 3,
  66.   FAILED: -1,
  67.  
  68.   // IQ stanza status codes:
  69.   IQ_WAIT: 0,
  70.   IQ_OK: 1,
  71.   IQ_ERROR: -1,
  72.  
  73.   _init: function( clientName, realm, clientPassword, transport, authenticator ) {
  74.     this._log = Log4Moz.Service.getLogger("Service.XmppClient");
  75.     this._myName = clientName;
  76.     this._realm = realm;
  77.     this._fullName = clientName + "@" + realm;
  78.     this._myPassword = clientPassword;
  79.     this._connectionStatus = this.NOT_CONNECTED;
  80.     this._error = null;
  81.     this._streamOpen = false;
  82.     this._transportLayer = transport;
  83.     this._authenticationLayer = authenticator;
  84.     this._log.debug("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword);
  85.     this._authenticationLayer.initialize( clientName, realm, clientPassword );
  86.     this._messageHandlers = [];
  87.     this._iqResponders = [];
  88.     this._nextIqId = 0;
  89.     this._pendingIqs = {};
  90.     this._callbackOnConnect = null;
  91.   },
  92.  
  93.   __parser: null,
  94.   get _parser() {
  95.     if (!this.__parser)
  96.       this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser );
  97.     return this.__parser;
  98.   },
  99.  
  100.   __threadManager: null,
  101.   get _threadManager() {
  102.     if (!this.__threadManager)
  103.       this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
  104.     return this.__threadManager;
  105.   },
  106.  
  107.   parseError: function( streamErrorNode ) {
  108.     var error = streamErrorNode.childNodes[0];
  109.     this._log.error( "Name: " + error.nodeName + " Value: " + error.nodeValue );
  110.     this._error = error.nodeName;
  111.     this.disconnect();
  112.     /* Note there can be an optional <text>bla bla </text> node inside
  113.        stream: error giving additional info; there can also optionally
  114.        be an app-specific condition element qualified by an app-defined
  115.        namespace */
  116.   },
  117.  
  118.   _finishConnectionAttempt: function() {
  119.     if ( this._callbackOnConnect ) {
  120.       this._callbackOnConnect.call();
  121.     }
  122.   },
  123.  
  124.   setError: function( errorText ) {
  125.     this._log.error( errorText );
  126.     this._error = errorText;
  127.     this._connectionStatus = this.FAILED;
  128.     this._finishConnectionAttempt();
  129.   },
  130.  
  131.   onIncomingData: function( messageText ) {
  132.     this._log.debug("onIncomingData(): rcvd: " + messageText);
  133.     var responseDOM = this._parser.parseFromString( messageText, "text/xml" );
  134.  
  135.     // Handle server disconnection
  136.     if (messageText.match("^</stream:stream>$")) {
  137.       this._handleServerDisconnection();
  138.       return;
  139.     }
  140.  
  141.     // Detect parse errors, and attempt to handle them in the valid cases.
  142.  
  143.     if (responseDOM.documentElement.nodeName == "parsererror" ) {
  144.       /*
  145.       Before giving up, remember that XMPP doesn't close the top-level
  146.       <stream:stream> element until the communication is done; this means
  147.       that what we get from the server is often technically only an
  148.       xml fragment.  Try manually appending the closing tag to simulate
  149.       a complete xml document and then parsing that. */
  150.  
  151.       var response = messageText + this._makeClosingXml();
  152.       responseDOM = this._parser.parseFromString( response, "text/xml" );
  153.     }
  154.  
  155.     if ( responseDOM.documentElement.nodeName == "parsererror" ) {
  156.       /* If that still doesn't work, it might be that we're getting a fragment
  157.       with multiple top-level tags, which is a no-no.  Try wrapping it
  158.       all inside one proper top-level stream element and parsing. */
  159.       response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml();
  160.       responseDOM = this._parser.parseFromString( response, "text/xml" );
  161.     }
  162.  
  163.     if ( responseDOM.documentElement.nodeName == "parsererror" ) {
  164.       /* Still can't parse it, give up. */
  165.       this.setError( "Can't parse incoming XML." );
  166.       return;
  167.     }
  168.  
  169.     // Message is parseable, now look for message-level errors.
  170.     var rootElem = responseDOM.documentElement;
  171.     var errors = rootElem.getElementsByTagName( "stream:error" );
  172.     if ( errors.length > 0 ) {
  173.       this.setError( errors[0].firstChild.nodeName );
  174.       return;
  175.     }
  176.     errors = rootElem.getElementsByTagName( "error" );
  177.     if ( errors.length > 0 ) {
  178.       this.setError( errors[0].firstChild.nodeName );
  179.       return;
  180.     }
  181.  
  182.     // Stream is valid.
  183.     // Detect and handle mid-authentication steps.
  184.     if ( this._connectionStatus == this.CALLED_SERVER ) {
  185.       // skip TLS, go straight to SALS. (encryption should be negotiated
  186.       // at the HTTP layer, i.e. use HTTPS)
  187.  
  188.       //dispatch whatever the next stage of the connection protocol is.
  189.       response = this._authenticationLayer.generateResponse( rootElem );
  190.       if ( response == false ) {
  191.         this.setError( this._authenticationLayer.getError() );
  192.       } else if ( response == this._authenticationLayer.COMPLETION_CODE ){
  193.         this._connectionStatus = this.CONNECTED;
  194.     this._finishConnectionAttempt();
  195.       } else {
  196.         this._transportLayer.send( response );
  197.       }
  198.       return;
  199.     }
  200.  
  201.     // Detect and handle regular communication.
  202.     if ( this._connectionStatus == this.CONNECTED ) {
  203.       var presences = rootElem.getElementsByTagName( "presence" );
  204.       if (presences.length > 0 ) {
  205.         var from = presences[0].getAttribute( "from" );
  206.         if ( from != undefined ) {
  207.           this._log.debug( "I see that " + from + " is online." );
  208.         }
  209.       }
  210.  
  211.       if ( rootElem.nodeName == "message" ) {
  212.         this.processIncomingMessage( rootElem );
  213.       } else {
  214.         var messages = rootElem.getElementsByTagName( "message" );
  215.         if (messages.length > 0 ) {
  216.           for ( var message in messages ) {
  217.             this.processIncomingMessage( messages[ message ] );
  218.           }
  219.         }
  220.       }
  221.  
  222.       if ( rootElem.nodeName == "iq" ) {
  223.         this.processIncomingIq( rootElem );
  224.       } else {
  225.         var iqs = rootElem.getElementsByTagName( "iq" );
  226.         if ( iqs.length > 0 ) {
  227.           for ( var iq in iqs ) {
  228.             this.processIncomingIq( iqs[ iq ] );
  229.           }
  230.         }
  231.       }
  232.     }
  233.   },
  234.  
  235.   processIncomingMessage: function( messageElem ) {
  236.     this._log.debug("processIncomingMsg: messageElem is a " + messageElem);
  237.     var from = messageElem.getAttribute( "from" );
  238.     var contentElem = messageElem.firstChild;
  239.     // Go down till we find the element with nodeType = 3 (TEXT_NODE)
  240.     while ( contentElem.nodeType != 3 ) {
  241.       contentElem = contentElem.firstChild;
  242.     }
  243.     this._log.debug("Incoming msg from " + from + ":" + contentElem.nodeValue);
  244.     for ( var x in this._messageHandlers ) {
  245.       // TODO do messages have standard place for metadata?
  246.       // will want to have handlers that trigger only on certain metadata.
  247.       this._messageHandlers[x].handle( contentElem.nodeValue, from );
  248.     }
  249.   },
  250.  
  251.   processIncomingIq: function( iqElem ) {
  252.     /* This processes both kinds of incoming IQ stanzas --
  253.        ones that are new (initated by another jabber client) and those that
  254.        are responses to ones we sent out previously.  We can tell the
  255.        difference by the type attribute. */
  256.     var buddy = iqElem.getAttribute( "from " );
  257.     var id = iqElem.getAttribute( id );
  258.  
  259.     switch( iqElem.getAttribute( "type" ) ) {
  260.     case "get":
  261.       /* Someone is asking us for the value of a variable.
  262.       Delegate this to the registered iqResponder; package the answer
  263.       up in an IQ stanza of the same ID and send it back to the asker. */
  264.       var variable = iqElem.firstChild.firstChild.getAttribute( "var" );
  265.       // TODO what to do here if there's more than one registered
  266.       // iqResponder?
  267.       var value = this._iqResponders[0].get( variable );
  268.       var query = "<query><getresult value='" + value + "'/></query>";
  269.       var xml = _makeIqXml( this._fullName, buddy, "result", id, query );
  270.       this._transportLayer.send( xml );
  271.     break;
  272.     case "set":
  273.       /* Someone is telling us to set the value of a variable.
  274.       Delegate this to the registered iqResponder; we can reply
  275.       either with an empty iq type="result" stanza, or else an
  276.       iq type="error" stanza */
  277.       var variable = iqElem.firstChild.firstChild.getAttribute( "var" );
  278.       var newValue = iqElem.firstChild.firstChildgetAttribute( "value" );
  279.       // TODO what happens when there's more than one reigistered
  280.       // responder?
  281.       // TODO give the responder a chance to say "no" and give an error.
  282.       this._iqResponders[0].set( variable, value );
  283.       var xml = _makeIqXml( this._fullName, buddy, "result", id, "<query/>" );
  284.       this._transportLayer.send( xml );
  285.     break;
  286.     case "result":
  287.       /* If all is right with the universe, then the id of this iq stanza
  288.       corresponds to a set or get stanza that we sent out, so it should
  289.       be in our pending dictionary.
  290.       */
  291.       if ( this._pendingIqs[ id ] == undefined ) {
  292.         this.setError( "Unexpected IQ reply id" + id );
  293.         return;
  294.       }
  295.       /* The result stanza may have a query with a value in it, in
  296.       which case this is the value of the variable we requested.
  297.       If there's no value, it was probably a set query, and should
  298.       just be considred a success. */
  299.       var newValue = iqElem.firstChild.firstChild.getAttribute( "value" );
  300.       if ( newValue != undefined ) {
  301.         this._pendingIqs[ id ].value = newValue;
  302.       } else {
  303.         this._pendingIqs[ id ].value = true;
  304.       }
  305.       this._pendingIqs[ id ].status = this.IQ_OK;
  306.       break;
  307.     case "error":
  308.       /* Dig down through the element tree till we find the one with
  309.       the error text... */
  310.       var elems = iqElem.getElementsByTagName( "error" );
  311.       var errorNode = elems[0].firstChild;
  312.       if ( errorNode.nodeValue != null ) {
  313.         this.setError( errorNode.nodeValue );
  314.       } else {
  315.         this.setError( errorNode.nodeName );
  316.       }
  317.       if ( this._pendingIqs[ id ] != undefined ) {
  318.         this._pendingIqs[ id ].status = this.IQ_ERROR;
  319.       }
  320.       break;
  321.     }
  322.   },
  323.  
  324.   registerMessageHandler: function( handlerObject ) {
  325.     /* messageHandler object must have
  326.        handle( messageText, from ) method.
  327.      */
  328.     this._messageHandlers.push( handlerObject );
  329.   },
  330.  
  331.   registerIQResponder: function( handlerObject ) {
  332.     /* IQResponder object must have
  333.        .get( variable ) and
  334.        .set( variable, newvalue ) methods. */
  335.     this._iqResponders.push( handlerObject );
  336.   },
  337.  
  338.   onTransportError: function( errorText ) {
  339.     this.setError( errorText );
  340.   },
  341.  
  342.   connect: function( host, callback ) {
  343.     /* Do the handshake to connect with the server and authenticate.
  344.        callback is optional: if provided, it will be called (with no arguments)
  345.        when the connection has either succeeded or failed. */
  346.     if ( callback ) {
  347.       this._callbackOnConnect = callback;
  348.     }
  349.     this._transportLayer.connect();
  350.     this._transportLayer.setCallbackObject( this );
  351.     this._transportLayer.send( this._makeHeaderXml( host ) );
  352.     this._connectionStatus = this.CALLED_SERVER;
  353.     // Now we wait... the rest of the protocol will be driven by
  354.     // onIncomingData.
  355.   },
  356.  
  357.   _makeHeaderXml: function( recipient ) {
  358.     return "<?xml version='1.0'?><stream:stream to='" +
  359.            recipient +
  360.            "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
  361.   },
  362.  
  363.   _makeMessageXml: function( messageText, fullName, recipient ) {
  364.     /* a "message stanza".  Note the message element must have the
  365.     full namespace info or it will be rejected. */
  366.     var msgXml = "<message xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" +
  367.                  fullName + "' to='" + recipient + "' xml:lang='en'><body>" +
  368.                  messageText + "</body></message>";
  369.     this._log.debug( "Outgoing Message xml: " + msgXml );
  370.     return msgXml;
  371.   },
  372.  
  373.   _makePresenceXml: function( fullName ) {
  374.     // a "presence stanza", sent to announce my presence to the server;
  375.     // the server is supposed to multiplex this to anyone subscribed to
  376.     // presence notifications.
  377.     return "<presence from ='" + fullName + "'><show/></presence>";
  378.   },
  379.  
  380.   _makeIqXml: function( fullName, recipient, type, id, query ) {
  381.     /* an "iq (info/query) stanza".  This can be used for structured data
  382.     exchange:  I send an <iq type='get' id='1'> containing a query,
  383.     and get back an <iq type='result' id='1'> containing the answer to my
  384.     query.  I can also send an <iq type='set' id='2'> to set a value
  385.     remotely.  The recipient answers with either <iq type='result'> or
  386.     <iq type='error'>, with an id matching the id of my set or get. */
  387.  
  388.     //Useful!!
  389.     return "<iq xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" + fullName + "' to='" + recipient + "' type='" + type + "' id='" + id + "'>" + query + "</iq>";
  390.   },
  391.  
  392.   _makeClosingXml: function () {
  393.     return "</stream:stream>";
  394.   },
  395.  
  396.   _generateIqId: function() {
  397.     // Each time this is called, it returns an ID that has not
  398.     // previously been used this session.
  399.     var id = "client_" + this._nextIqId;
  400.     this._nextIqId = this._nextIqId + 1;
  401.     return id;
  402.   },
  403.  
  404.   _sendIq: function( recipient, query, type ) {
  405.     var id = this._generateIqId();
  406.     this._pendingIqs[ id ] = { status: this.IQ_WAIT };
  407.     this._transportLayer.send( this._makeIqXml( this._fullName,
  408.             recipient,
  409.             type,
  410.             id,
  411.             query ) );
  412.     /* And then wait for a response with the same ID to come back...
  413.        When we get a reply, the pendingIq dictionary entry will have
  414.        its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and
  415.        this was a query that's supposed to return a value, the value
  416.        will be in the value field of the entry. */
  417.     var thread = this._threadManager.currentThread;
  418.     while( this._pendingIqs[ id ].status == this.IQ_WAIT ) {
  419.       thread.processNextEvent( true );
  420.     }
  421.     if ( this._pendingIqs[ id ].status == this.IQ_OK ) {
  422.       return this._pendingIqs[ id ].value;
  423.     } else if ( this._pendingIqs[ id ].status == this.IQ_ERROR ) {
  424.       return false;
  425.     }
  426.     // Can't happen?
  427.   },
  428.  
  429.   iqGet: function( recipient, variable ) {
  430.     var query = "<query><getvar var='" + variable + "'/></query>";
  431.     return this._sendIq( recipient, query, "get" );
  432.   },
  433.  
  434.   iqSet: function( recipient, variable, value ) {
  435.     var query = "<query><setvar var='" + variable + "' value='" + value + "'/></query>";
  436.     return this._sendIq( recipient, query, "set" );
  437.   },
  438.  
  439.   sendMessage: function( recipient, messageText ) {
  440.     // OK so now I'm doing that part, but what am I supposed to do with the
  441.     // new JID that I'm bound to??
  442.     var body = this._makeMessageXml( messageText, this._fullName, recipient );
  443.     this._transportLayer.send( body );
  444.   },
  445.  
  446.   announcePresence: function() {
  447.     this._transportLayer.send( this._makePresenceXml(this._myName) );
  448.   },
  449.  
  450.   subscribeForPresence: function( buddyId ) {
  451.     // OK, there are 'subscriptions' and also 'rosters'...?
  452.     //this._transportLayer.send( "<presence to='" + buddyId + "' type='subscribe'/>" );
  453.     // TODO
  454.     // other side must then approve this by sending back a presence to
  455.     // me with type ='subscribed'.
  456.   },
  457.  
  458.   disconnect: function() {
  459.     // todo: only send closing xml if the stream has not already been
  460.     // closed (if there was an error, the server will have closed the stream.)
  461.     this._transportLayer.send( this._makeClosingXml() );
  462.  
  463.     this.waitForDisconnect();
  464.   },
  465.  
  466.   _handleServerDisconnection: function() {
  467.     this._transportLayer.disconnect();
  468.     this._connectionStatus = this.NOT_CONNECTED;
  469.   },
  470.  
  471.   waitForConnection: function( ) {
  472.     var thread = this._threadManager.currentThread;
  473.     while ( this._connectionStatus != this.CONNECTED &&
  474.       this._connectionStatus != this.FAILED ) {
  475.       thread.processNextEvent( true );
  476.     }
  477.   },
  478.  
  479.   waitForDisconnect: function() {
  480.     var thread = this._threadManager.currentThread;
  481.     while ( this._connectionStatus == this.CONNECTED ) {
  482.       thread.processNextEvent( true );
  483.     }
  484.   }
  485. };
  486.